package com.zytech.zspider.core;

import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zytech.modules.lang.Langs;
import com.zytech.modules.lang.Requires;
import com.zytech.modules.lang.Strings;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

/**
 * @author siuming
 */
public class TaskDefinition implements Serializable {

    private static final long serialVersionUID = -2562083988041152914L;

    /**
     * Task definition settings.
     */
    public final class Settings {

        public static final String HTTP_PROXY = "http.proxy";
        public static final String HTTP_TIMEOUT = "http.timeout";
        public static final String HTTP_RETRIES = "http.retries";
        public static final String HTTP_ENCODING = "http.encoding";
        public static final String HTTP_EXECUTOR = "http.executor";
        public static final String HTTP_CSS_ENABLED = "http.css.enabled";
        public static final String HTTP_SCRIPT_ENABLED = "http.script.enabled";
        public static final String HTTP_BROWSER_VERSION = "http.browser.version";
        public static final String HTTP_FOLLOW_REDIRECT = "http.follow.redirect";
        public static final String HTTP_FOLLOW_REDIRECT_METHODS = "http.follow.redirect.methods";

        private Settings() {
        }
    }

    /**
     * empty definition.
     */
    public static final TaskDefinition EMPTY = null;

    private final String name;
    private final String url;
    private final String method;

    private String body;
    private Map<String, String> params = Maps.newHashMap();
    private Map<String, String> headers = Maps.newHashMap();
    private Map<String, String> settings = Maps.newHashMap();
    private Map<String, String> collectExprs = Maps.newHashMap();

    private String startExpr;
    private String finishExpr;
    private TaskResultPipelineSettings pipelineSettings;

    private List<TaskDefinition> subtasks = Lists.newArrayList();
    private List<TaskGenDefinition> subtaskGenDefinitions = Lists.newArrayList();

    /**
     * @param name
     * @param url
     * @param method
     */
    TaskDefinition(String name, String url, String method) {
        this.name = name;
        this.url = url;
        this.method = method;
    }

    /**
     * @return
     */
    public String getName() {
        return name;
    }

    /**
     * @return
     */
    public String getUrl() {
        return url;
    }

    /**
     * @return
     */
    public String getMethod() {
        return method;
    }

    /**
     * @return
     */
    public String getBody() {
        return body;
    }

    /**
     * @return
     */
    public boolean isEmptyBody() {
        return Strings.isBlank(body);
    }

    /**
     * @param body
     * @return
     */
    public TaskDefinition body(String body) {
        this.body = body;
        return this;
    }

    /**
     * @return
     */
    public Map<String, String> getParams() {
        return ImmutableMap.copyOf(params);
    }

    /**
     * @param name
     * @param value
     * @return
     */
    public TaskDefinition param(String name, String value) {
        Requires.hasText(name, "name must not be null or empty.");
        this.params.put(name, Langs.nullSafe(value, ""));
        return this;
    }

    /**
     * @param params
     * @return
     */
    public TaskDefinition params(Map<String, String> params) {
        Requires.notNull(params, "params must not be null.");
        for (Map.Entry<String, String> entry : params.entrySet()) {
            param(entry.getKey(), entry.getValue());
        }
        return this;
    }

    /**
     * @param name
     * @return
     */
    public TaskDefinition dropParam(String name) {
        Requires.hasText(name, "name must not be null.");
        this.params.remove(name);
        return this;
    }

    /**
     * @param names
     * @return
     */
    public TaskDefinition dropParams(List<String> names) {
        Requires.notNull(names, "names must not be null.");
        for (String name : names) {
            dropParam(name);
        }
        return this;
    }

    /**
     * @return
     */
    public TaskDefinition dropAllParams() {
        this.params.clear();
        return this;
    }

    /**
     * @param params
     * @return
     */
    public TaskDefinition diffParams(Map<String, String> params) {
        Requires.notNull(params, "params must not be null.");
        Map<String, String> diff = Maps.filterKeys(params, new Predicate<String>() {
            @Override
            public boolean apply(String name) {
                return !TaskDefinition.this.params.containsKey(name);
            }
        });

        return params(diff);
    }

    /**
     * @return
     */
    public Map<String, String> getHeaders() {
        return ImmutableMap.copyOf(headers);
    }

    /**
     * @param name
     * @param value
     * @return
     */
    public TaskDefinition header(String name, String value) {
        Requires.hasText(name, "name must not be null or empty.");
        this.headers.put(name, Langs.nullSafe(value, ""));
        return this;
    }

    /**
     * @param headers
     * @return
     */
    public TaskDefinition headers(Map<String, String> headers) {
        Requires.notNull(headers, "headers must not be null.");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            header(entry.getKey(), entry.getValue());
        }
        return this;
    }

    /**
     * @param name
     * @return
     */
    public TaskDefinition dropHeader(String name) {
        Requires.hasText(name, "name must not be null or empty.");
        this.headers.remove(name);
        return this;
    }

    /**
     * @param names
     * @return
     */
    public TaskDefinition dropHeaders(List<String> names) {
        Requires.notNull(names, "names must not be null.");
        for (String name : names) {
            dropHeader(name);
        }
        return this;
    }

    /**
     * @return
     */
    public TaskDefinition dropAllHeaders() {
        this.headers.clear();
        return this;
    }

    /**
     * @param headers
     * @return
     */
    public TaskDefinition diffHeaders(Map<String, String> headers) {
        Requires.notNull(headers, "headers must not be null.");
        Map<String, String> diff = Maps.filterKeys(headers, new Predicate<String>() {
            @Override
            public boolean apply(String name) {
                return !TaskDefinition.this.headers.containsKey(name);
            }
        });

        return headers(diff);
    }

    /**
     * @return
     */
    public Map<String, String> getSettings() {
        return ImmutableMap.copyOf(settings);
    }

    /**
     * @param name
     * @param value
     * @return
     */
    public TaskDefinition setting(String name, String value) {
        Requires.hasText(name, "name must not be null or empty.");
        this.settings.put(name, Langs.nullSafe(value, ""));
        return this;
    }

    /**
     * @param settings
     * @return
     */
    public TaskDefinition settings(Map<String, String> settings) {
        Requires.notNull(settings, "settings must not be null.");
        for (Map.Entry<String, String> entry : settings.entrySet()) {
            setting(entry.getKey(), entry.getValue());
        }
        return this;
    }

    /**
     * @param settings
     * @return
     */
    public TaskDefinition diffSettings(Map<String, String> settings) {
        Requires.notNull(settings, "settings must not be null.");
        Map<String, String> diff = Maps.filterKeys(settings, new Predicate<String>() {
            @Override
            public boolean apply(String name) {
                return !TaskDefinition.this.settings.containsKey(name);
            }
        });

        return settings(diff);
    }

    /**
     * @return
     */
    public Map<String, String> getCollectExprs() {
        return ImmutableMap.copyOf(collectExprs);
    }

    /**
     * @param name
     * @param expr
     * @return
     */
    public TaskDefinition collect(String name, String expr) {
        Requires.hasText(name, "name must not be null or empty.");
        Requires.hasText(expr, "expr must not be null or empty.");
        this.collectExprs.put(name, expr);
        return this;
    }

    /**
     * @param collectExprs
     * @return
     */
    public TaskDefinition collects(Map<String, String> collectExprs) {
        Requires.notNull(collectExprs, "collectExprs must not be null.");
        for (Map.Entry<String, String> entry : collectExprs.entrySet()) {
            collect(entry.getKey(), entry.getValue());
        }
        return this;
    }

    /**
     * @return
     */
    public String getStartExpr() {
        return startExpr;
    }

    /**
     * @param startupExpr
     * @return
     */
    public TaskDefinition onStart(String startupExpr) {
        this.startExpr = startupExpr;
        return this;
    }

    /**
     * @return
     */
    public boolean isEmptyStartExpr() {
        return Strings.isBlank(getStartExpr());
    }

    /**
     * @return
     */
    public String getFinishExpr() {
        return finishExpr;
    }

    /**
     * @param finishExpr
     * @return
     */
    public TaskDefinition onFinish(String finishExpr) {
        this.finishExpr = finishExpr;
        return this;
    }

    /**
     * @return
     */
    public boolean isEmptyFinishExpr() {
        return Strings.isBlank(getFinishExpr());
    }

    /**
     * @param pipelineSettings
     * @return
     */
    public TaskDefinition onResult(TaskResultPipelineSettings pipelineSettings) {
        this.pipelineSettings = pipelineSettings;
        return this;
    }

    /**
     * @return
     */
    public TaskResultPipelineSettings getPipelineSettings() {
        return pipelineSettings;
    }

    /**
     * @return
     */
    public boolean isEmptyPipelineSettings() {
        return null == pipelineSettings;
    }

    /**
     * @param taskGenDefinition
     * @return
     */
    public TaskDefinition subtaskGenDefinition(TaskGenDefinition taskGenDefinition) {
        Requires.notNull(taskGenDefinition, "taskGenDefinition must not be null.");
        this.subtaskGenDefinitions.add(taskGenDefinition);
        return this;
    }

    /**
     * @param taskGenDefinitions
     * @return
     */
    public TaskDefinition subtaskGenDefinitions(List<TaskGenDefinition> taskGenDefinitions) {
        Requires.notNull(taskGenDefinitions, "taskGenDefinitions must not be null.");
        for (TaskGenDefinition taskGenDefinition : taskGenDefinitions) {
            subtaskGenDefinition(taskGenDefinition);
        }
        return this;
    }

    /**
     * @return
     */
    public List<TaskGenDefinition> getSubtaskGenDefinitions() {
        return ImmutableList.copyOf(subtaskGenDefinitions);
    }

    /**
     * @return
     */
    public boolean isEmptySubtaskGenDefinitions() {
        return subtaskGenDefinitions.isEmpty();
    }

    /**
     * @return
     */
    public List<TaskDefinition> getSubtasks() {
        return ImmutableList.copyOf(subtasks);
    }

    /**
     * @return
     */
    public boolean isEmptySubtasks() {
        return subtasks.isEmpty();
    }

    /**
     * @param subtask
     * @return
     */
    public TaskDefinition subtask(TaskDefinition subtask) {
        Requires.notNull(subtask, "subtask must not be null.");
        this.subtasks.add(subtask);
        return this;
    }

    /**
     * @param subtasks
     * @return
     */
    public TaskDefinition subtasks(List<TaskDefinition> subtasks) {
        Requires.notNull(subtasks, "subtasks must not be null.");
        this.subtasks.addAll(subtasks);
        return this;
    }

    /**
     * @return
     */
    public TaskDefinition copy() {
        return Langs.clone(this);
    }

    @Override
    public String toString() {
        return Objects
                .toStringHelper(this)
                .add("name", getName())
                .add("url", getUrl())
                .add("method", getMethod())
                .add("params", getParams())
                .add("headers", getHeaders())
                .add("settings", getSettings())
                .toString();
    }

    /**
     * @param name
     * @param url
     * @return
     */
    public static TaskDefinition of(String name, String url) {
        return of(name, url, "GET");
    }

    /**
     * @param name
     * @param url
     * @param method
     * @return
     */
    public static TaskDefinition of(String name, String url, String method) {
        Requires.hasText(name, "name must not be null or empty.");
        Requires.hasText(url, "url must not be null or empty.");
        Requires.hasText(method, "method must not be null or empty.");
        return new TaskDefinition(name, url, method);
    }
}